<?php
/**
 * This file houses the XsdSchema class.
 *
 * Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this list
 *   of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, this 
 *   list of conditions and the following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 * - Neither the name of the Kalkomey Enterprises, Inc. nor the names of its 
 *   contributors may be used to endorse or promote products derived from this 
 *   software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @copyright Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * @link http://code.google.com/p/swerv
 * @package Shared
 * @subpackage Protocols
 */

/**
 * The XsdSchema class is used to generate an XSD for a specific controller and action.  It includes all the complex types for the request objects, response objects, and models.
 *
 * @package Shared
 * @subpackage Protocols
 */
class XsdSchema {

	/**
	 * A list of simple types that do not need a complex type definition.
	 *
	 * @var array
	 */
	private $simpleTypes;

	/**
	 * An instance of the XMLWriter class.
	 *
	 * @var XMLWriter
	 */
	private $xmlWriter;

	/**
	 * A list of complex types that need a complex type definition.
	 *
	 * @var array
	 */
	private $complexTypes;

	/**
	 * Object constructor.
	 *
	 * @return XsdSchema
	 */
	public function __construct() {
		$this->simpleTypes = array (
			'string',
			'bool',
			'boolean',
			'int',
			'integer',
			'float',
			'double',
			'decimal'
		);
		$this->complexTypes = array();
		$this->xmlWriter = new XMLWriter();
		$this->xmlWriter->openMemory();
	}

	/**
	 * Returns an XSD for the given controller/action.
	 * If an XSD is found in the schemas directory (format: $className_$method.xsd) then the contents of that file will be returned.
	 * 
	 * @uses StringUtility::toLowerCase()
	 * @uses StringUtility::formatXmlString()
	 * @uses SiteContext::$baseUrl
	 * @uses ClassReflector::reflect()
	 * @uses XsdSchema::getAllComplexTypes()
	 * @uses XsdSchema::writeHeader()
	 * @uses XsdSchema::writeComplexTypes()
	 * @uses XsdSchema::writeFooter()
	 *
	 * @param string $className the class name of the controller
	 * @param string $metod the method name of the action
	 *
	 * @return string
	 */
	public function getSchema($className, $method) {
		$filename = str_replace('\\', '/', dirname(__FILE__)) . '/schemas/' . $className . '_' . $method . '.xsd';
		if (file_exists($filename)) {
			return file_get_contents($filename);
		}
		$urlController = str_replace('_controller', '', StringUtility::toLowerCase($className));
		$urlMethod = StringUtility::toLowerCase($method);
		$url = SiteContext::$baseUrl . '/xml/' . $urlController . '/' . $urlMethod;
		$reflector = new ClassReflector($className);
		$reflection = $reflector->reflect();
		$parameters = $reflection['methods'][$method]['parameters'];
		$key = key($parameters);
		$requestType = $parameters[$key]['type'];
		$responseType = $reflection['methods'][$method]['return_type'];

		// get all complex types
		$this->getAllComplexTypes($requestType);
		$this->getAllComplexTypes($responseType);
		sort($this->complexTypes);

		// write header
		$this->writeHeader($url);

		// write complex types
		$this->writeComplexTypes();

		// write footer
		$this->writeFooter();

		$xml = $this->xmlWriter->flush();
		$xml = StringUtility::formatXmlString($xml);
		return $xml;
	}

	/**
	 * Builds a list of all the complex types need for this XSD and stores them in the XsdSchema::$complexTypes array.
	 * Note: this method is recursive
	 *
	 * @param string $type the type to define in the XSD (cannot be a simple type)
	 *
	 * @return void
	 */
	private function getAllComplexTypes($type) {
		// is it an array??
		if ($this->isTypeArray($type)) {
			$type = $this->getArrayType($type);
		}
		if (!in_array($type, $this->simpleTypes)) {
			if (!in_array($type, $this->complexTypes)) {
				$this->complexTypes[] = $type;
				$reflector = new ClassReflector($type);
				$reflection = $reflector->reflect();
				$parameters = $reflection['properties'];
				foreach ($parameters as $param => $info) {
					$this->getAllComplexTypes($info['type']);
				}
			}
		}
	}

	/**
	 * Determines if a given type is an array.
	 * Arrays and types are indicated in DocBlocks as @var array Type
	 *
	 * @returns bool
	 */
	private function isTypeArray($type) {
		$returnVal = false;
		if (substr($type, 0, 5) == 'array') {
			$returnVal = true;
		}
		return $returnVal;
	}

	/**
	 * Returns the type of objects stored in a given array type.
	 * Arrays and types are indicated in DocBlocks as @var array Type
	 *
	 * @returns string
	 */
	private function getArrayType($type) {
		return substr($type, 6, strlen($type));
	}

	/**
	 * Writes all of the complex types to the XSD by using the XMLWriter stored in XsdSchema::xmlWriter.
	 * Note: this is a recursive method
	 *
	 * @uses ClassReflector::reflect()
	 *
	 * @return void
	 */
	private function writeComplexTypes() {
		foreach ($this->complexTypes as $type) {
			$this->xmlWriter->startElement('xs:element');
			$this->xmlWriter->writeAttribute('name', $type);
			$this->xmlWriter->writeAttribute('type', $type);
			$this->xmlWriter->endElement();
		}
		foreach ($this->complexTypes as $type) {

			$reflection = new ClassReflector($type);
			$reflectionInfo = $reflection->reflect();
			$properties = $reflectionInfo['properties'];
			ksort($properties);

			$this->xmlWriter->startElement('xs:complexType');
			$this->xmlWriter->writeAttribute('name', $type);
			$this->xmlWriter->startElement('xs:sequence');

			foreach ($properties as $propertyName => $info) {
				$this->writeSimpleType($propertyName, $info['type']);
			}

			$this->xmlWriter->endElement();
			$this->xmlWriter->endElement();
		}
	}

	/**
	 * Writes a simple type to the XSD by using the XMLWriter stored in XsdSchema::xmlWriter.
	 *
	 * @uses XsdSchema::isTypeArray()
	 * @uses XsdSchema::getArrayType()
	 *
	 * @return void
	 */
	private function writeSimpleType($name, $type) {
		$isMulti = false;
		if ($this->isTypeArray($type)) {
			$type = $this->getArrayType($type);
			$isMulti = true;
		}
		$this->xmlWriter->startElement('xs:element');
		$this->xmlWriter->writeAttribute('name', $name);
		switch ($type) {
			case 'bool':
			case 'boolean':
				$this->xmlWriter->writeAttribute('type', 'xs:boolean');
				break;
			case 'int':
			case 'integer':
				$this->xmlWriter->writeAttribute('type', 'xs:integer');
				break;
			case 'float':
			case 'double':
			case 'decimal':
				$this->xmlWriter->writeAttribute('type', 'xs:decimal');
				break;
			case 'string':
				$this->xmlWriter->writeAttribute('type', 'xs:string');
				break;
			default:
				$this->xmlWriter->writeAttribute('type', $type);
				break;
		}
		$this->xmlWriter->writeAttribute('minOccurs', 1);
		if ($isMulti) {
			$this->xmlWriter->writeAttribute('maxOccurs', 'unbounded');
		} else {
			$this->xmlWriter->writeAttribute('maxOccurs', 1);
		}
		$this->xmlWriter->endElement();
	}

	/**
	 * Writes the header.
	 *
	 * @param string $url the URL used to access the given controller/action.
	 *
	 * @return void
	 */
	private function writeHeader($url) {
		$this->xmlWriter->startElement('xs:schema');
		$this->xmlWriter->writeAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
		$this->xmlWriter->writeAttribute('xmlns', $url);
		$this->xmlWriter->writeAttribute('targetNamespace', $url);
		$this->xmlWriter->writeAttribute('elementFormDefault', 'qualified');
	}

	/**
	 * Writes the footer.
	 *
	 * @return void
	 */
	private function writeFooter() {
		$this->xmlWriter->endElement();
	}

}
